package game;

import static org.lwjgl.opengl.GL11.GL_MODELVIEW;
import static org.lwjgl.opengl.GL11.GL_PROJECTION;
import static org.lwjgl.opengl.GL11.glLoadIdentity;
import static org.lwjgl.opengl.GL11.glMatrixMode;
import static org.lwjgl.opengl.GL11.glOrtho;

import java.awt.Canvas;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.List;

import javax.imageio.ImageIO;

import log.Logger;
import opengl.program.D2GLProgram;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.util.glu.GLU;

import texture.AnimationMetadataSection;
import texture.AnimationMetadataSectionSerializer;
import texture.DefaultResourcePack;
import texture.MetadataSerializer;
import texture.ReloadableResourceManager;
import texture.ResourcePack;
import texture.SimpleReloadableResourceManager;
import texture.Stitcher;
import texture.TextureManager;
import texture.TextureMap;
import texture.TextureMetadataSection;
import texture.TextureMetadataSectionSerializer;
import world.Vertex;

public class StitcherMain {
	private int vaoId = 0;
	private int vboId = 0;
	private int vboiId = 0;
	private int indicesCount = 0;

	// Shader variables
	private int vsId = 0;
	private int fsId = 0;
	private int pId = 0;

	private TextureManager textureManager;
	//private final List<Tickable> listTickables = new ArrayList<>();

	// Texture variables
	protected D2GLProgram program;

	private StitcherPanel stitcherPanel;
	private Canvas stitcherCanvas;
	private StitcherStatus statusTree = StitcherStatus.get();

	public static void main(String[] args) {
		new StitcherMain();
	}

	private StitcherMain() {
		this.setupUI();
		this.setupOpenGL();
		this.setupShaders();
		this.setupTextures();
		this.setupQuad();

		while (!shouldStop()) {
			loopCycle();
			Display.sync(60);
			Display.update();
		}

		destroyOpenGL();
	}

	private void setupUI(){
		stitcherPanel = new StitcherPanel();
		stitcherPanel.buildUI();
		stitcherCanvas = stitcherPanel.mainCanvas;
	}
	
	private void destroyOpenGL() {
		GL20.glUseProgram(0);
		GL20.glDetachShader(pId, vsId);
		GL20.glDetachShader(pId, fsId);

		GL20.glDeleteShader(vsId);
		GL20.glDeleteShader(fsId);
		GL20.glDeleteProgram(pId);

		// Select the VAO
		GL30.glBindVertexArray(vaoId);

		// Disable the VBO index from the VAO attributes list
		GL20.glDisableVertexAttribArray(0);
		GL20.glDisableVertexAttribArray(1);

		// Delete the vertex VBO
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
		GL15.glDeleteBuffers(vboId);

		// Delete the index VBO
		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
		GL15.glDeleteBuffers(vboiId);

		// Delete the VAO
		GL30.glBindVertexArray(0);
		GL30.glDeleteVertexArrays(vaoId);

		Display.destroy();
		stitcherPanel.dispose();
	}

	private void loopEvent() {
		eventChangeInputFile();
		eventSavingStitcher();
	}

	private void loopCycle() {
		GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);

		textureManager.tick();
		program.begin();
		{
			GL13.glActiveTexture(GL13.GL_TEXTURE0);
			GL30.glBindVertexArray(vaoId);
			GL20.glEnableVertexAttribArray(0);
			GL20.glEnableVertexAttribArray(1);
			GL20.glEnableVertexAttribArray(2);

			GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboiId);

			GL11.glDrawElements(GL11.GL_TRIANGLES, indicesCount, GL11.GL_UNSIGNED_BYTE, 0);

			GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
			GL20.glDisableVertexAttribArray(0);
			GL20.glDisableVertexAttribArray(1);
			GL20.glDisableVertexAttribArray(2);
			GL30.glBindVertexArray(0);
		}
		program.end();
		loopEvent();
		this.exitOnGLError("loopCycle");
	}

	public void setupQuad() {
		TextureMap  textureMap = textureManager.getTextureMap(TextureMap.locationBlocksTexture);
		float width =  4*(float) textureMap.stitcher.getCurrentWidth() / (float) stitcherCanvas.getWidth();
		float height = 4*(float) textureMap.stitcher.getCurrentHeight() / (float) stitcherCanvas.getHeight();
		Vertex v0 = new Vertex().setXYZ(-width / 2, +height / 2, 0).setRGB(1, 0, 0).setST(0, 0);
		Vertex v1 = new Vertex().setXYZ(-width / 2, -height / 2, 0).setRGB(0, 1, 0).setST(0, 1);
		Vertex v2 = new Vertex().setXYZ(+width / 2, -height / 2, 0).setRGB(0, 0, 1).setST(1, 1);
		Vertex v3 = new Vertex().setXYZ(+width / 2, +height / 2, 0).setRGB(1, 1, 1).setST(1, 0);

		Vertex[] vertices = new Vertex[] { v0, v1, v2, v3 };
		FloatBuffer verticesBuffer = BufferUtils.createFloatBuffer(vertices.length * Vertex.SIZE_ALL);
		for (int i = 0; i < vertices.length; i++) {
			verticesBuffer.put(vertices[i].getElements());
		}
		verticesBuffer.flip();

		byte[] indices = { 0, 1, 2, 2, 3, 0 };
		indicesCount = indices.length;
		ByteBuffer indicesBuffer = BufferUtils.createByteBuffer(indicesCount);
		indicesBuffer.put(indices);
		indicesBuffer.flip();

		vaoId = GL30.glGenVertexArrays();
		GL30.glBindVertexArray(vaoId);

		// Create a new Vertex Buffer Object in memory and select it (bind)
		vboId = GL15.glGenBuffers();
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);
		GL15.glBufferData(GL15.GL_ARRAY_BUFFER, verticesBuffer, GL15.GL_STATIC_DRAW);

		GL20.glVertexAttribPointer(0, Vertex.SIZE_POSITION, GL11.GL_FLOAT, false, Vertex.STRIDE, Vertex.OFFSET_POSITION);
		GL20.glVertexAttribPointer(1, Vertex.SIZE_COLOR, GL11.GL_FLOAT, false, Vertex.STRIDE, Vertex.OFFSET_COLOR);
		GL20.glVertexAttribPointer(2, Vertex.SIZE_TEXTURE, GL11.GL_FLOAT, false, Vertex.STRIDE, Vertex.OFFSET_TEXTURE);

		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);

		GL30.glBindVertexArray(0);

		// Create a new VBO for the indices and select it (bind) - INDICES
		vboiId = GL15.glGenBuffers();
		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboiId);
		GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL15.GL_STATIC_DRAW);
		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);

		this.exitOnGLError("setupQuad");
	}

	private void setupTextures() {
		List<ResourcePack> defaultResourcePacks = new ArrayList<>();
		DefaultResourcePack resourcePack = new DefaultResourcePack(new File("./assets/"));
		defaultResourcePacks.add(resourcePack);

		MetadataSerializer metadataSerializer = new MetadataSerializer();
		metadataSerializer.registerMetadataSectionType(new TextureMetadataSectionSerializer(), TextureMetadataSection.class);
		metadataSerializer.registerMetadataSectionType(new AnimationMetadataSectionSerializer(), AnimationMetadataSection.class);

		ReloadableResourceManager theResourceManager = new SimpleReloadableResourceManager(metadataSerializer);
		theResourceManager.reloadResources(new ArrayList<ResourcePack>(defaultResourcePacks));

		textureManager = TextureManager.get(theResourceManager);
		textureManager.loadTextureMap(TextureMap.locationBlocksTexture, new TextureMap(0, "textures/blocks"));
	}

	private void setupShaders() {
		try {
			program = new D2GLProgram();
			program.init();
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}
		this.exitOnGLError("setupShaders");
	}

	private void setupOpenGL() {
		try {
			int w = stitcherCanvas.getWidth();
			int h = stitcherCanvas.getHeight();
			double aspectRatio = (double) w / h;

			Display.setParent(stitcherCanvas);
			Display.setVSyncEnabled(true);
			PixelFormat pixelFormat = new PixelFormat();
			ContextAttribs contextAtrributes = new ContextAttribs(3, 1);
			Display.setDisplayMode(new DisplayMode(w, h));
			Display.create(pixelFormat, contextAtrributes);

			GL11.glViewport(0, 0, w, h);
			glMatrixMode(GL_PROJECTION);
			glLoadIdentity();
			glOrtho(aspectRatio, -aspectRatio, -1.0d, 1.0d, 1.0d, -1.0d);
			glMatrixMode(GL_MODELVIEW);
			glLoadIdentity();
			GL11.glClearColor(0.4f, 0.6f, 0.9f, 0f);
		} catch (LWJGLException e) {
			e.printStackTrace();
			System.exit(-1);
		}
		this.exitOnGLError("setupOpenGL");
	}

	private void eventChangeInputFile() {
		if (!statusTree.inputFileChangedYN) {
			return;
		}

		statusTree.inputFileChangedYN = false;
		this.setupTextures();
		this.setupQuad();
	}

	private void eventSavingStitcher() {

		if (!statusTree.outputFileSaveYN) {
			return;
		}

		statusTree.outputFileSaveYN = false;
		eventSavingStitcher();

		TextureMap  textureMap = textureManager.getTextureMap(TextureMap.locationBlocksTexture);
		Stitcher stitcher = textureMap.stitcher;
		int width = stitcher.getCurrentWidth();
		int height = stitcher.getCurrentHeight();
		int bpp = 4; // Assuming a 32-bit display with a byte each for red, green, blue, and alpha.
		ByteBuffer buffer = BufferUtils.createByteBuffer(width * height * bpp);
		GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer);
		String filePath = stitcherPanel.getOutputFileName();
		File file = new File(filePath); // The file to save to.
		String format = "PNG";
		BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

		for (int x = 0; x < width; x++) {
			for (int y = 0; y < height; y++) {
				int i = (x + (width * y)) * bpp;
				int r = buffer.get(i) & 0xFF;
				int g = buffer.get(i + 1) & 0xFF;
				int b = buffer.get(i + 2) & 0xFF;
				image.setRGB(x, height - (y + 1), (0xFF << 24) | (r << 16) | (g << 8) | b);
			}
		}

		try {
			ImageIO.write(image, format, file);
		} catch (IOException ec) {
			ec.printStackTrace();
		}
	}

	private boolean shouldStop() {
		return Keyboard.isKeyDown(Keyboard.KEY_ESCAPE) || statusTree.closeRequestedYN;
	}

	private void destroy() {
		if (Display.isCreated()) {
			Display.destroy();
		}
		System.exit(0);
	}

	private void exitOnGLError(String errorMessage) {
		int errorValue = GL11.glGetError();
		if (errorValue == GL11.GL_NO_ERROR) {
			return;
		}

		String errorString = GLU.gluErrorString(errorValue);
		Logger.error("ERROR - " + errorMessage + ": " + errorString);
		destroy();
	}
}
